﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Collections;
using System.Text.RegularExpressions;

namespace TA.JSON
{
	/// <summary>
	/// Outputs JSON text.
	/// </summary>
	public class JsonWriter
	{
		#region PUBLIC
		#region DECLARATIONS
		#endregion
		#region INIT
		/// <summary>
		/// Ctor.
		/// </summary>
		/// <param name="tw">Destination.</param>
		public JsonWriter(TextWriter tw)
		{
			this._tw = tw;
			this._write = this.WriteTW;
			this.ExternalStringsVariable = "__JE";
		}
		/// <summary>
		/// Ctor.
		/// </summary>
		/// <param name="sb">Destination.</param>
		public JsonWriter(StringBuilder sb)
		{
			this._sb = sb;
			this._write = this.WriteSB;
			//this._tw = new StringWriter(sb);
		}
		#endregion
		#region PROPS
		/// <summary>
		/// Gets or sets the name of the variable which will hold the external strings.
		/// </summary>
		public string ExternalStringsVariable{get;set;}
		/// <summary>
		/// Gets the writer's current state.
		/// </summary>
		public JsonState CurrentState
		{
			get
			{
				return this._currentState;
			}
		}
		/// <summary>
		/// Gets the list of external strings which this JsonWriter has recorded so far.
		/// </summary>
		public IEnumerable<string> ExternalStrings
		{
			get
			{
				return this._externalStrings;
			}
		}
		#endregion
		#region METHODS
		/// <summary>
		/// Outputs the specified string as raw text.
		/// </summary>
		/// <param name="str"></param>
		public void Write(string str)
		{
			this._write(str);	
		}
		/// <summary>
		/// Writes the object start token ('{') to the output and adjusts the current state accordingly.
		/// </summary>
		public void StartObject()
		{
			this.VerifyStateValue();

			//	this._CheckArrayStart();
			this.CheckStart();

			this.PushState(JsonState.Object);
			this.Write(JsonConst.StartObject);
		}
		/// <summary>
		/// Writes the object end token ('}') to the output and adjusts the current state accordingly.
		/// </summary>
		public void EndObject()
		{
			this.VerifyState(new[] { JsonState.Object });

			this.PopState();
			this.Write("}");
		}
		/// <summary>
		/// Writes the array start token ('[') to the output and adjusts the current state accordingly.
		/// </summary>
		public void StartArray()
		{
			this.VerifyStateValue();

			this.CheckStart();
			this.PushState(JsonState.Array);
			this.Write(JsonConst.StartArray);
		}
		/// <summary>
		/// Writes the array end token (']') to the output and adjusts the current state accordingly.
		/// </summary>
		public void EndArray()
		{
			this.VerifyState(new[] { JsonState.Array });

			this.PopState();
			this.Write(JsonConst.EndArray);
		}
		/// <summary>
		/// Writes the specified property name followed by the separator symbol (':').
		/// </summary>
		/// <param name="name"></param>
		public void PropertyName(string name)
		{
			this.PropertyName(name, false);
		}
		/// <summary>
		/// Writes the specified property name followed by the separator symbol (':').
		/// </summary>
		/// <param name="name"></param>
		/// <param name="forceEscape">If true, property name is always escaped (even if valid w/o quotes).</param>
		public void PropertyName(string name, bool forceEscape)
		{
			this.VerifyState(new[] { JsonState.Object });
			this.CheckStart();

			this.PushState(JsonState.ObjectValue);
			if (_rgxPropertyValid.IsMatch(name) && !forceEscape)
			{
				this.Write(name);
			}
			else
			{
				this.Write(name.JsonEscape());
			}
			this.Write(JsonConst.PropertyEquals);
		}
		/// <summary>
		/// Writes the specified value.
		/// </summary>
		/// <param name="value"></param>
		public void Value(object value)
		{

			if (value == null)
				this.Null();
			else if (value is IJsonSerializable)
				this.Value((IJsonSerializable)value);
			// unsigned integers
			else if (value is byte)
				this.Value((ulong)(byte)value);
			else if (value is ushort)
				this.Value((ulong)(ushort)value);
			else if (value is uint)
				this.Value((ulong)(uint)value);
			else if (value is ulong)
				this.Value((ulong)value);
			// signed integers
			else if (value is sbyte)
				this.Value((long)(sbyte)value);
			else if (value is short)
				this.Value((long)(short)value);
			else if (value is int)
				this.Value((long)(int)value);
			else if (value is long)
				this.Value((long)value);
			// other
			else if (value is DateTime)
				this.Value((DateTime)value);
			else if (value is bool)
				this.Value((bool)value);
			else if (value is string)
				this.Value((string)value);
			else if (value.GetType().IsEnum)
				this.Value((long)Convert.ChangeType(value, typeof(long)));
			else
				throw new ArgumentException("Failed to get JSON for value " + value.ToString());

		}
		/// <summary>
		/// Writes "null".
		/// </summary>
		public void Null()
		{
			this.Value_Begin();
			this.Write("null");
			this.Value_End();
		}
		/// <summary>
		/// Serializes the specified value by calling its Serialize method.
		/// </summary>
		/// <param name="value"></param>
		public void Value(IJsonSerializable value)
		{
			Value_Begin();

			value.Serialize(this);

			Value_End();
		}
		/// <summary>
		/// Writes the specified long value.
		/// </summary>
		/// <param name="value"></param>
		public void Value(long value)
		{
			this.Value_Begin();
			this.Write(value.ToString());
			this.Value_End();
		}
		/// <summary>
		/// Writes the specified unsigned long value.
		/// </summary>
		/// <param name="value"></param>
		public void Value(ulong value)
		{
			this.Value_Begin();
			this.Write(value.ToString());
			this.Value_End();
		}
		/// <summary>
		/// Writes the specified string value, taking care of quotation and escaping.
		/// </summary>
		/// <param name="value"></param>
		public void Value(string value)
		{
			this.Value(value, false);
		}
		/// <summary>
		/// Writes the specified string value, taking care of quotation and escaping. Optionally, adds it as an external.
		/// </summary>
		/// <param name="value"></param>
		public void Value(string value, bool external)
		{
			if (value == null)
			{
				this.Null();
				return;
			}
			this.Value_Begin();

			if (external)
			{
				this._externalStrings.Add(value);

				this.Write(this.ExternalStringsVariable);
				this.Write("[");
				this.Write((this._externalStrings.Count - 1).ToString());
				this.Write("]");
			}
			else
			{
				this.Write(value.JsonEscape());
			}

			this.Value_End();
		}
		/// <summary>
		/// Writes the specified boolean value.
		/// </summary>
		/// <param name="value"></param>
		public void Value(bool value)
		{
			this.Value_Begin();
			this.Write(value.ToString().ToLower());
			this.Value_End();
		}
		/// <summary>
		/// Writes the specified DateTime value in the form "new Date(y, M, d, h, m, s, ms)".
		/// </summary>
		/// <param name="value"></param>
		public void Value(DateTime value)
		{
			this.Value_Begin();
			this.Write(string.Format("new Date({0}, {1}, {2}, {3}, {4}, {5}, {6})", value.Year, value.Month, value.Day, value.Hour, value.Minute, value.Second, value.Millisecond));
			this.Value_End();
		}
		#endregion
		#endregion
		#region PRIVATE
		#region CONST/STATIC
		static readonly Regex _rgxPropertyValid = new Regex(@"[a-zA-Z_$](?:[a-zA-Z_$0-9])*");
		static readonly string[] _builtinKeywords = new[] { "if", "else", "while", "do", "if", "class", "function", "this" };
		#endregion
		#region VARS
		TextWriter _tw;
		StringBuilder _sb;
		Action<string> _write;
		Stack<JsonState> _stack = new Stack<JsonState>();
		JsonState _currentState;
		bool _first = true, _firstDone = false;
		List<string> _externalStrings = new List<string>();
		#endregion
		#region INIT
		#endregion
		#region PROPS
		#endregion
		#region METHODS
		void PushState(JsonState s)
		{
			if (this._currentState == JsonState.ObjectValue)
				this._stack.Pop();
			this._currentState = s;
			this._stack.Push(s);
			this._first = true; this._firstDone = false;
		}
		void PopState()
		{
			this._stack.Pop();
			this._first = false; this._firstDone = false;
			if (this._stack.Count == 0)
				this._currentState = JsonState.None;
			else
				this._currentState = this._stack.Peek();
		}
		void VerifyState(JsonState[] states)
		{
			for (var i = 0; i < states.Length; ++i)
			{
				if (states[i] == this._currentState)
					return;
			}

			StringBuilder sbStates = new StringBuilder("Expected states ");
			foreach (JsonState s in states)
			{
				sbStates.Append(s.ToString());
				sbStates.Append(", ");
			}
			sbStates.Append("but encountered ");
			sbStates.Append(this._currentState);
			throw new InvalidOperationException(sbStates.ToString());
		}
		void VerifyStateValue()
		{
			this.VerifyState(new[] { JsonState.None, JsonState.ObjectValue, JsonState.Array });
		}
		void CheckStart()
		{
			if (this._currentState == JsonState.Array || this._currentState == JsonState.Object)
			{
				if (!this._first && !this._firstDone)
					this.Write(",");

				this._first = false;
				this._firstDone = true;
			}
		}
		private void Value_Begin()
		{
			this.VerifyStateValue();
			this.CheckStart();
		}
		private void Value_End()
		{
			if (this._currentState == JsonState.ObjectValue)
				this.PopState();
			this._firstDone = false;
		}
		void WriteSB(string str)
		{
			this._sb.Append(str);
		}
		void WriteTW(string str)
		{
			this._tw.Write(str);
		}
		#endregion
		#endregion
	}
}
